/*
*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.genie.core.jpa.services;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jsonpatch.JsonPatch;
import com.github.fge.jsonpatch.JsonPatchException;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.netflix.genie.common.dto.Application;
import com.netflix.genie.common.dto.Cluster;
import com.netflix.genie.common.dto.ClusterStatus;
import com.netflix.genie.common.dto.Command;
import com.netflix.genie.common.dto.CommandStatus;
import com.netflix.genie.common.exceptions.GenieBadRequestException;
import com.netflix.genie.common.exceptions.GenieConflictException;
import com.netflix.genie.common.exceptions.GenieException;
import com.netflix.genie.common.exceptions.GenieNotFoundException;
import com.netflix.genie.common.exceptions.GeniePreconditionException;
import com.netflix.genie.common.exceptions.GenieServerException;
import com.netflix.genie.core.jpa.entities.ApplicationEntity;
import com.netflix.genie.core.jpa.entities.ClusterEntity;
import com.netflix.genie.core.jpa.entities.CommandEntity;
import com.netflix.genie.core.jpa.repositories.JpaApplicationRepository;
import com.netflix.genie.core.jpa.repositories.JpaClusterRepository;
import com.netflix.genie.core.jpa.repositories.JpaCommandRepository;
import com.netflix.genie.core.jpa.specifications.JpaClusterSpecs;
import com.netflix.genie.core.jpa.specifications.JpaCommandSpecs;
import com.netflix.genie.core.services.CommandService;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.annotation.Transactional;
import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Implementation of the CommandService interface.
*
* @author amsharma
* @author tgianos
* @since 2.0.0
*/
@Transactional(
rollbackFor = {
GenieException.class,
ConstraintViolationException.class
}
)
@Slf4j
public class JpaCommandServiceImpl implements CommandService {
private final ObjectMapper mapper = new ObjectMapper();
private final JpaCommandRepository commandRepo;
private final JpaApplicationRepository appRepo;
private final JpaClusterRepository clusterRepo;
/**
* Default constructor.
*
* @param commandRepo the command repository to use
* @param appRepo the application repository to use
* @param clusterRepo the cluster repository to use
*/
public JpaCommandServiceImpl(
final JpaCommandRepository commandRepo,
final JpaApplicationRepository appRepo,
final JpaClusterRepository clusterRepo
) {
this.commandRepo = commandRepo;
this.appRepo = appRepo;
this.clusterRepo = clusterRepo;
}
/**
* {@inheritDoc}
*/
@Override
public String createCommand(
@NotNull(message = "No command entered. Unable to create.")
@Valid
final Command command
) throws GenieException {
log.debug("Called to create command {}", command);
final Optional<String> commandId = command.getId();
if (commandId.isPresent() && this.commandRepo.exists(commandId.get())) {
throw new GenieConflictException("A command with id " + commandId.get() + " already exists");
}
final CommandEntity commandEntity = new CommandEntity();
commandEntity.setId(command.getId().orElse(UUID.randomUUID().toString()));
this.updateAndSaveCommandEntity(commandEntity, command);
return commandEntity.getId();
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(readOnly = true)
public Command getCommand(
@NotBlank(message = "No id entered unable to get.")
final String id
) throws GenieException {
log.debug("called");
return this.findCommand(id).getDTO();
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(readOnly = true)
public Page<Command> getCommands(
final String name,
final String user,
final Set<CommandStatus> statuses,
final Set<String> tags,
final Pageable page
) {
log.debug("Called");
@SuppressWarnings("unchecked")
final Page<CommandEntity> commandEntities = this.commandRepo.findAll(
JpaCommandSpecs.find(
name,
user,
statuses,
tags
),
page
);
return commandEntities.map(CommandEntity::getDTO);
}
/**
* {@inheritDoc}
*/
@Override
public void updateCommand(
@NotBlank(message = "No id entered. Unable to update.")
final String id,
@NotNull(message = "No command information entered. Unable to update.")
@Valid
final Command updateCommand
) throws GenieException {
if (!this.commandRepo.exists(id)) {
throw new GenieNotFoundException("No command exists with the given id. Unable to update.");
}
final Optional<String> updateId = updateCommand.getId();
if (updateId.isPresent() && !id.equals(updateId.get())) {
throw new GenieBadRequestException("Command id inconsistent with id passed in.");
}
log.debug("Called to update command with id {} {}", id, updateCommand);
this.updateAndSaveCommandEntity(this.findCommand(id), updateCommand);
}
/**
* {@inheritDoc}
*/
@Override
public void patchCommand(@NotBlank final String id, @NotNull final JsonPatch patch) throws GenieException {
final CommandEntity commandEntity = this.findCommand(id);
try {
final Command commandToPatch = commandEntity.getDTO();
log.debug("Will patch command {}. Original state: {}", id, commandToPatch);
final JsonNode commandNode = this.mapper.readTree(commandToPatch.toString());
final JsonNode postPatchNode = patch.apply(commandNode);
final Command patchedCommand = this.mapper.treeToValue(postPatchNode, Command.class);
log.debug("Finished patching command {}. New state: {}", id, patchedCommand);
this.updateAndSaveCommandEntity(commandEntity, patchedCommand);
} catch (final JsonPatchException | IOException e) {
log.error("Unable to patch cluster {} with patch {} due to exception.", id, patch, e);
throw new GenieServerException(e.getLocalizedMessage(), e);
}
}
/**
* {@inheritDoc}
*/
@Override
public void deleteAllCommands() throws GenieException {
log.debug("Called to delete all commands");
for (final CommandEntity commandEntity : this.commandRepo.findAll()) {
this.deleteCommand(commandEntity.getId());
}
}
/**
* {@inheritDoc}
*/
@Override
public void deleteCommand(
@NotBlank(message = "No id entered. Unable to delete.")
final String id
) throws GenieException {
log.debug("Called to delete command config with id {}");
final CommandEntity commandEntity = this.findCommand(id);
//Remove the command from the associated Application references
final List<ApplicationEntity> originalApps = commandEntity.getApplications();
if (originalApps != null) {
final List<ApplicationEntity> applicationEntities = Lists.newArrayList(originalApps);
applicationEntities.forEach(commandEntity::removeApplication);
}
//Remove the command from the associated cluster references
final Set<ClusterEntity> originalClusters = commandEntity.getClusters();
if (originalClusters != null) {
final Set<ClusterEntity> clusterEntities = Sets.newHashSet(originalClusters);
clusterEntities.forEach(clusterEntity -> clusterEntity.removeCommand(commandEntity));
}
this.commandRepo.delete(commandEntity);
}
/**
* {@inheritDoc}
*/
@Override
public void addConfigsForCommand(
@NotBlank(message = "No command id entered. Unable to add configurations.")
final String id,
@NotEmpty(message = "No configuration files entered. Unable to add.")
final Set<String> configs
) throws GenieException {
this.findCommand(id).getConfigs().addAll(configs);
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(readOnly = true)
public Set<String> getConfigsForCommand(
@NotBlank(message = "No command id entered. Unable to get configs.")
final String id
) throws GenieException {
return this.findCommand(id).getConfigs();
}
/**
* {@inheritDoc}
*/
@Override
public void updateConfigsForCommand(
@NotBlank(message = "No command id entered. Unable to update configurations.")
final String id,
@NotEmpty(message = "No configs entered. Unable to update.")
final Set<String> configs
) throws GenieException {
this.findCommand(id).setConfigs(configs);
}
/**
* {@inheritDoc}
*/
@Override
public void removeAllConfigsForCommand(
@NotBlank(message = "No command id entered. Unable to remove configs.")
final String id
) throws GenieException {
this.findCommand(id).getConfigs().clear();
}
/**
* {@inheritDoc}
*/
@Override
public void removeConfigForCommand(
@NotBlank(message = "No command id entered. Unable to remove configuration.")
final String id,
@NotBlank(message = "No config entered. Unable to remove.")
final String config
) throws GenieException {
this.findCommand(id).getConfigs().remove(config);
}
/**
* {@inheritDoc}
*/
@Override
public void addTagsForCommand(
@NotBlank(message = "No command id entered. Unable to add tags.")
final String id,
@NotEmpty(message = "No tags entered. Unable to add.")
final Set<String> tags
) throws GenieException {
final CommandEntity command = this.findCommand(id);
final Set<String> commandTags = command.getTags();
commandTags.addAll(tags);
command.setTags(commandTags);
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(readOnly = true)
public Set<String> getTagsForCommand(
@NotBlank(message = "No command id sent. Cannot retrieve tags.")
final String id
) throws GenieException {
return this.findCommand(id).getTags();
}
/**
* {@inheritDoc}
*/
@Override
public void updateTagsForCommand(
@NotBlank(message = "No command id entered. Unable to update tags.")
final String id,
@NotEmpty(message = "No tags entered. Unable to update.")
final Set<String> tags
) throws GenieException {
this.findCommand(id).setTags(tags);
}
/**
* {@inheritDoc}
*/
@Override
public void removeAllTagsForCommand(
@NotBlank(message = "No command id entered. Unable to remove tags.")
final String id
) throws GenieException {
this.findCommand(id).setTags(Sets.newHashSet());
}
/**
* {@inheritDoc}
*/
@Override
public void removeTagForCommand(
@NotBlank(message = "No command id entered. Unable to remove tag.")
final String id,
@NotBlank(message = "No tag entered. Unable to remove.")
final String tag
) throws GenieException {
final CommandEntity command = this.findCommand(id);
final Set<String> commandTags = command.getTags();
commandTags.remove(tag);
command.setTags(commandTags);
}
/**
* {@inheritDoc}
*/
@Override
public void addApplicationsForCommand(
@NotBlank(message = "No command id entered. Unable to add applications.")
final String id,
@NotEmpty(message = "No application ids entered. Unable to add applications.")
final List<String> applicationIds
) throws GenieException {
if (applicationIds.size() != applicationIds.stream().filter(this.appRepo::exists).count()) {
throw new GeniePreconditionException("All applications need to exist to add to a command");
}
final CommandEntity commandEntity = this.findCommand(id);
for (final String appId : applicationIds) {
commandEntity.addApplication(this.appRepo.findOne(appId));
}
}
/**
* {@inheritDoc}
*/
@Override
public void setApplicationsForCommand(
@NotBlank(message = "No command id entered. Unable to set applications.")
final String id,
@NotNull(message = "No application ids entered. Unable to set applications.")
final List<String> applicationIds
) throws GenieException {
if (applicationIds.size() != applicationIds.stream().filter(this.appRepo::exists).count()) {
throw new GeniePreconditionException("All applications need to exist to add to a command");
}
final CommandEntity commandEntity = this.findCommand(id);
final List<ApplicationEntity> applicationEntities = new ArrayList<>();
applicationIds.forEach(appId -> applicationEntities.add(this.appRepo.findOne(appId)));
commandEntity.setApplications(applicationEntities);
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(readOnly = true)
public List<Application> getApplicationsForCommand(
@NotBlank(message = "No command id entered. Unable to get applications.")
final String id
) throws GenieException {
final CommandEntity commandEntity = this.findCommand(id);
return commandEntity
.getApplications()
.stream()
.map(ApplicationEntity::getDTO)
.collect(Collectors.toList());
}
/**
* {@inheritDoc}
*/
@Override
public void removeApplicationsForCommand(
@NotBlank(message = "No command id entered. Unable to remove applications.")
final String id
) throws GenieException {
this.findCommand(id).setApplications(null);
}
/**
* {@inheritDoc}
*/
@Override
public void removeApplicationForCommand(
@NotBlank(message = "No command id entered. Unable to remove application.")
final String id,
@NotBlank(message = "No application id entered. Unable to remove application.")
final String appId
) throws GenieException {
final CommandEntity commandEntity = this.findCommand(id);
final ApplicationEntity applicationEntity = this.appRepo.findOne(appId);
if (applicationEntity != null) {
commandEntity.getApplications().remove(applicationEntity);
} else {
throw new GenieNotFoundException("No application with id " + id + " exists.");
}
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(readOnly = true)
public Set<Cluster> getClustersForCommand(
@NotBlank(message = "No command id entered. Unable to get clusters.")
final String id,
final Set<ClusterStatus> statuses
) throws GenieException {
if (!this.commandRepo.exists(id)) {
throw new GenieNotFoundException("No command with id " + id + " exists.");
}
@SuppressWarnings("unchecked")
final List<ClusterEntity> clusterEntities = this.clusterRepo.findAll(
JpaClusterSpecs.findClustersForCommand(
id,
statuses
)
);
return clusterEntities
.stream()
.map(ClusterEntity::getDTO)
.collect(Collectors.toSet());
}
/**
* Helper method to find a command entity.
*
* @param id The id of the command to find
* @return The command entity if one is found
* @throws GenieNotFoundException When the command doesn't exist
*/
private CommandEntity findCommand(final String id) throws GenieNotFoundException {
final CommandEntity commandEntity = this.commandRepo.findOne(id);
if (commandEntity != null) {
return commandEntity;
} else {
throw new GenieNotFoundException("No command with id " + id + " exists.");
}
}
private void updateAndSaveCommandEntity(final CommandEntity commandEntity, final Command command) {
commandEntity.setName(command.getName());
commandEntity.setUser(command.getUser());
commandEntity.setVersion(command.getVersion());
final Optional<String> description = command.getDescription();
commandEntity.setDescription(description.isPresent() ? description.get() : null);
commandEntity.setExecutable(command.getExecutable());
commandEntity.setCheckDelay(command.getCheckDelay());
commandEntity.setConfigs(command.getConfigs());
final Optional<String> setupFile = command.getSetupFile();
commandEntity.setSetupFile(setupFile.isPresent() ? setupFile.get() : null);
commandEntity.setStatus(command.getStatus());
commandEntity.setTags(command.getTags());
final Optional<Integer> memory = command.getMemory();
commandEntity.setMemory(memory.isPresent() ? memory.get() : null);
this.commandRepo.save(commandEntity);
}
}